Skip to content

fix(lint): os validate and os build resolve a report's dataset through the reading package's declared closure, as the runtime does - #33171

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-nlni1h
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-nlni1h

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #33128

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:多包产物逐包检查时,报表(含 joined 块、列表视图图表、页面图表)引用的 dataset 改为和看板一样按「本包 → 已声明依赖闭包里唯一持有者」解析;依赖包的数据集不再被 os validate / os build 误报 chart-dataset-unknown。
  • 为什么改:运行时本来就按 callerPackage 从依赖闭包解析报表的数据集并正常返回,作者期却只看本包自己的 datasets,有效的模块化应用构建不过(看板那一半已由 fix(lint)!: os validate and os build resolve a dashboard's dataset through the reader's declared closure, as the runtime does #33121 修好,这是报表那一半)。
  • 风险与回滚:只放行运行时本来就放行的引用;闭包外的持有者、闭包内两个持有者仍然报错(现在会点名持有者并给出可行改法),发布门不变,运行时不变。回滚即 revert 本 PR。
  • 先看:packages/lint/src/validate-chart-bindings.ts 的 resolveDataset(拒绝与警告两支)和 indexDatasets(改读看板同一份解析结果)。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-chart-bindings.test.ts,预期 91/91 通过,其中 [#33128] 一组 11 条。
  • 需要你做的:无,席位落地。

Red lines

  1. Resolution is never loosened. The binding resolves through indexArtifactDatasetResolution, the resolution widget-dataset-unknown and report-filter-field-unknown already read; this PR adds no second walk and no wider reach. A dataset only an undeclared package holds stays refused: REFUSES a report over a dataset only an UNDECLARED package holds, naming the holder and the dependency to declare, and the CONTROL half of ACCEPTS the same dependency’s dataset on a joined block, a list-view chart and a page chart. The phase-1 one-holder tests are not touched (packages/core, packages/objectql are outside the diff); bare-name URL behaviour is not touched (no runtime file in the diff).
  2. Ambiguity is reported, never guessed. REFUSES a name two packages of the closure hold, naming both: nothing picks one (red line 2) asserts the code, the severity, the path and the message's first clause naming both holders, and that the report's positions are judged against neither. REPORTS, not refuses, when the reader declares a dependency the artifact does not carry; never softens an ambiguity keeps it an error when an outside dependency is declared. Ablation A4 (the resolver picks the first holder) turns both red.
  3. Independently releasable. One package (@objectstack/lint, patch), no runtime answer moves, nothing in this diff waits on a sibling card. No migration: nothing stored changes. No behaviour moves between layers, so no ADR-0087 entry is owed (the authoring gate stops refusing what the runtime serves).

What changes

Door FROM (main 47f6c93) TO
os validate / os build / os lint, per-package pass: a report, joined block, list-view chart or page chart bound to a dataset a package it declares holds (transitively) chart-dataset-unknown error, "resolves to no declared dataset … Declared datasets: (none)" accepted; its rows, values and chart axes are judged against that dataset
same, a dataset only a package outside the declared closure holds the same generic error chart-dataset-unknown error naming the holder and the open fix (declare the dependency, or move when the holder already depends on the reader): the dashboard rule's sentence
same, a dataset two packages inside the closure hold the same generic error chart-dataset-unknown error naming both; nothing picks one
same, outside the closure, and the reader declares a dependency the artifact does not carry the same generic error chart-dataset-unknown warning: not judged, that package may hold it once installed (the dashboard rule's S1 arm)
the union run with a residual top-level datasets beside the composition (manifest: 'preserve') a module's report on the module's own dataset refused accepted: the union run reads every body, as the dashboard rule does
a dataset no package declares; a stack without packages[]; the publish door (its write snapshot carries no packages[]) error / unchanged unchanged

Why every surface of this rule and not only reports: every reader it judges sends its own served package as callerPackage (DatasetReportRenderer, ObjectChart, the list view's chart in objectViewListRenderer.tsx), and the dataset doors resolve the name from that package by ADR-0150 D3: "?callerPackage=P names the package of the item that reads the name (a dashboard, report or chart of P) … The name resolves from P by D3" (ADR-0150, amendment note of 2026-11-11). On the per-package pass every item is the judged package's, so one resolution serves all three surfaces.

Which resolver: the order points at packages/core/src/package-scoped-resolution.ts. PR #33121's dashboard half resolves with indexArtifactDatasetResolution (packages/lint/src/validate-widget-bindings.ts), over packageResolutionClosure (@objectstack/metadata-core); core's module holds the D3 rule's runtime vocabulary, which the lint does not call. This PR imports that same lint function and its two wording functions; nothing is copied, and neither package-scoped-resolution.ts nor validate-widget-bindings.ts is edited.

validate-report-runtime-filter.ts (review nit N1 on PR #33121), re-judged: it does not share the defect. It has read indexResolvableDatasets, the same closure-aware index, since #33121, so a report's runtimeFilter over a declared dependency's dataset already resolves that dataset. No change. Its per-package pass leaves a refused name unjudged, and chart-dataset-unknown now carries that verdict with the reason; the union run still judges the filter.

Clause-② reads no: the lint now accepts only what the runtime and ADR-0150 D3 already accept (the quotation above); no published contract refuses it. Metadata-protocol reads none: no key, value or type is added or removed, no meaning or default changes (the authoring gate stops a false refusal); copied from the claim.

Tests

packages/lint/src/validate-chart-bindings.test.ts, one new top-level describe, [#33128] chart-dataset-unknown resolves through the reading package’s declared closure (11 cases):

  • ACCEPTS a report over the dataset of a package it declares, and judges its positions against it
  • resolves a TRANSITIVE dependency’s dataset (the declared closure, ADR-0141)
  • ACCEPTS the same dependency’s dataset on a joined block, a list-view chart and a page chart (CONTROL: each refused without the dependency)
  • REFUSES a report over a dataset only an UNDECLARED package holds, naming the holder and the dependency to declare (code, severity, where, path, full message)
  • names the move, never the dependency, when the holder already depends on the reader (a cycle)
  • REFUSES a name two packages of the closure hold, naming both: nothing picks one (red line 2)
  • the reading package’s own dataset wins a name its dependency also holds (ADR-0150 D3 step 1)
  • REPORTS, not refuses, when the reader declares a dependency the artifact does not carry; never softens an ambiguity
  • a name NO package declares, and any stack judged without the artifact, keep the unchanged sentence
  • the union run reads every body, as the dashboard rule does: a residual top-level datasets no longer hides the module’s own (CONTROL: the app's per-package pass is refused with the cycle hint)
  • reaches the rule table the per-package pass runs; the publish door’s snapshot is unchanged

Ablations (mutation through scripts/ablation-replace.mjs, red seen, restored with blob == HEAD and git diff HEAD empty; the suite imports src/, so no dist leg):

A1 · the BASE index back (`stack.datasets` alone)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 0180ff82bd96 -> 5f0360b3f627
     × ACCEPTS a report over the dataset of a package it declares, and judges its positions against it
     × resolves a TRANSITIVE dependency’s dataset (the declared closure, ADR-0141)
     × ACCEPTS the same dependency’s dataset on a joined block, a list-view chart and a page chart
     × a name NO package declares, and any stack judged without the artifact, keep the unchanged sentence
     × the union run reads every body, as the dashboard rule does: a residual top-level `datasets` no longer hides the module’s own
     × reaches the rule table the per-package pass runs; the publish door’s snapshot is unchanged
      Tests  6 failed | 85 passed (91)
ablation-replace: ok restored: blob == HEAD (0180ff82bd96) and `git diff HEAD` is empty
A2 · the refusal arm dropped (a refused name falls to the generic sentence)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 0180ff82bd96 -> 6cc59c84f5e3
     × REFUSES a report over a dataset only an UNDECLARED package holds, naming the holder and the dependency to declare
     × names the move, never the dependency, when the holder already depends on the reader (a cycle)
     × REFUSES a name two packages of the closure hold, naming both: nothing picks one (red line 2)
     × REPORTS, not refuses, when the reader declares a dependency the artifact does not carry; never softens an ambiguity
     × the union run reads every body, as the dashboard rule does: a residual top-level `datasets` no longer hides the module’s own
      Tests  5 failed | 86 passed (91)
ablation-replace: ok restored: blob == HEAD (0180ff82bd96) and `git diff HEAD` is empty
A3 · every refusal softened to the warning
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 0180ff82bd96 -> 4e44dcc56680
     × REFUSES a report over a dataset only an UNDECLARED package holds, naming the holder and the dependency to declare
     × names the move, never the dependency, when the holder already depends on the reader (a cycle)
     × REFUSES a name two packages of the closure hold, naming both: nothing picks one (red line 2)
     × REPORTS, not refuses, when the reader declares a dependency the artifact does not carry; never softens an ambiguity
     × the union run reads every body, as the dashboard rule does: a residual top-level `datasets` no longer hides the module’s own
     × reaches the rule table the per-package pass runs; the publish door’s snapshot is unchanged
      Tests  6 failed | 85 passed (91)
ablation-replace: ok restored: blob == HEAD (0180ff82bd96) and `git diff HEAD` is empty
A4 · the shared resolver picks the first of two closure holders (validate-widget-bindings.ts, mutation only)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 1254a2305c2e -> 0cdf4a3f09e9
     × REFUSES a name two packages of the closure hold, naming both: nothing picks one (red line 2)
     × REPORTS, not refuses, when the reader declares a dependency the artifact does not carry; never softens an ambiguity
      Tests  2 failed | 89 passed (91)
ablation-replace: ok restored: blob == HEAD (1254a2305c2e) and `git diff HEAD` is empty

Acceptance notes

  • Measured first at origin/main 47f6c93, validateChartBindings on the per-package shape (module com.example.m declares com.example.d, which holds ds; the module's report reads ds): one chart-dataset-unknown error, "binds dataset "ds", which resolves to no declared dataset — there is no data to render." / "Declared datasets: (none)."
  • Grep of the surface for ruling|Ruling-ref|maintainer|⛔: validate-chart-bindings.ts:93 names the maintainer ruling on chart-field-unknown (presentation positions warn, query positions error); this diff moves no position's severity. validate-report-runtime-filter.ts: no hit. package-scoped-resolution.ts: not edited; no export was needed.
  • validate-report-date-range.ts (not in this surface) indexes stack.datasets alone, so on the per-package pass a report's dateRange over a declared dependency's dataset is not judged there; the union run judges it. Not a false refusal and no verdict lost; noted, not filed.
  • Filter keys on a dependency's dataset (both report-filter-field-unknown and the dashboard twin): the per-package object graph holds the package's own objects, so a key on an object the dependency holds is skipped on that pass and judged on the union run. Probe at this head: a report over the dependency's dataset with runtimeFilter: { nope: 'x' } gives no per-package finding. The twin behaves the same; noted, not filed.
  • A top-level residual report beside a composition is judged on the union run against every body, as the dashboard rule judges a residual dashboard since spec+cli: an artifact's per-package checks refuse an action → flow and a widget → dataset reference that another package of the SAME artifact satisfies (the #18202 family, for flows and datasets) #25514; unchanged in kind, now the same for both rules.

Verification

All at head 60edc5ee, worktree off origin/main 47f6c93 (git merge origin/main: already up to date).

  • pnpm --filter @objectstack/lint build && … typecheck && … test (under os-verify-lock): VERDICT command-exit 0, Test Files 208 passed (208), Tests 8836 passed (8836); typecheck is tsc --noEmit && pnpm check:test-typecheck.
  • pnpm lint (repo-wide eslint): exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 59 families, each run with stdin from /dev/null, exit codes captured before any pipe; --ran: "59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded an exit code and none of them is 3)". Three first runs were prerequisite failures, not reds, and are recorded at their rerun: check-plugin-teardown-shape.mjs --self-test exit 3 on the shallow clone (its pinned fixture commit unreachable), exit 0 after git fetch --unshallow; check:dts-closure exit 1 and check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, no dist/) after an OS_SKIP_DTS=1 CLI build of mine, both exit 0 after a full pnpm build (VERDICT command-exit 0).
  • Dogfood battery: n/a, no ! in the title and no (narrowing) arm.
os validate through the built CLI, BASE by ablation into dist

A scratch objectstack.config.mjs: composeStacks([library, sales], { manifest: 'preserve' }), the library module holding dataset rptl_book_metrics, the sales app's summary report reading it; declared = sales lists the library in dependencies.

# this head
mode=declared exit=0
  ✓ Validation passed (147ms)
mode=undeclared exit=1
  • package 'com.qa.rpt.sales' — report "rpts_books_by_genre": dataset "rptl_book_metrics" is held by package "com.qa.rpt.lib", outside the declared dependency closure of package "com.qa.rpt.sales", so the installed report's read of it is refused 409 (ADR-0150 D3).
      fix: Declare the dependency: add "com.qa.rpt.lib" to the `dependencies` of package "com.qa.rpt.sales" (ADR-0141), or move the dataset into "com.qa.rpt.sales".
      rule: chart-dataset-unknown  at reports[0].dataset

# A1 (the BASE index) in dist: lint rebuilt, preflight "marker present in 4 built files"
mode=declared exit=1
  • package 'com.qa.rpt.sales' — report "rpts_books_by_genre": binds dataset "rptl_book_metrics", which resolves to no declared dataset — there is no data to render.
      rule: chart-dataset-unknown  at reports[0].dataset
# restored: blob == HEAD, rebuilt, preflight --absent "marker absent from all 20 built files", tree clean

A first try at this leg carried the marker in a line comment, which the bundle strips: its preflight answered "marker ABSENT" and that reading was discarded; the leg above carries the marker in a string literal.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…h the reading package's declared closure, as the runtime does

chart-dataset-unknown read the judged stack's own datasets alone, so on the
per-package pass a report, joined block, list-view chart or page chart over
a declared dependency's dataset was refused while the runtime served it.
It now reads the resolution the dashboard rule reads
(indexArtifactDatasetResolution) and takes that rule's refusal and
unverified sentences for a name the runtime refuses the reader.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvtG32iBab1PR1fpR6
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5e250a65992192bdc9b3fb3be304a2fca245b2d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f44820166e09b621622a5b7ed4920806a122a632 — the merge of head 60edc5ee5bdec64faf79dbd47fd0530618a89b33 into base 5e250a65992192bdc9b3fb3be304a2fca245b2d1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f44820166e09b621622a5b7ed4920806a122a632 && git checkout f44820166e09b621622a5b7ed4920806a122a632
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e250a65992192bdc9b3fb3be304a2fca245b2d1 60edc5ee5bdec64faf79dbd47fd0530618a89b33 && git checkout -B drift-repro 5e250a65992192bdc9b3fb3be304a2fca245b2d1 && git merge --no-ff 60edc5ee5bdec64faf79dbd47fd0530618a89b33

node scripts/docs-audit/affected-docs.mjs --json 5e250a65992192bdc9b3fb3be304a2fca245b2d1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um

席位核对 · ACCEPT · PR #33171 (#33128) · 2026-11-14T00:16Z

Head-sha: 60edc5ee5bdec64faf79dbd47fd0530618a89b33 · report 6867844916 · claim 6867125211

  • Form: draft → main, first body line Fixes #33128 (the card's whole acceptance is met: report, joined block, list-view and page charts resolve through the reader's declared closure; validate-report-runtime-filter.ts re-judged). No closing keyword beside another open card. Files: .changeset/33128-report-dataset-reader-closure.md, packages/lint/src/validate-chart-bindings.ts (+56 −10), its test (+197). Inside the claimed surface; package-scoped-resolution.ts untouched.
  • Premise measured: at 47f6c939 the per-package pass refuses a report on a declared dependency's dataset (chart-dataset-unknown, "Declared datasets: (none)"), in source and through the built CLI (os validate, ablated BASE into dist).
  • Red lines (epic design(metadata): address behavioural metadata by package — key flows, actions, jobs and other non-object types by (package, name); re-open ADR-0028 (replaces #30806 phase 3 renames) #31527): ① no second walk: the rule reads indexArtifactDatasetResolution, the one resolution widget-dataset-unknown and report-filter-field-unknown read; an undeclared holder stays refused (pinned). ② two closure holders → error naming both, the warning arm never softens it; ablation A4 (resolver picks the first holder) turns both pins red. ③ one package patch, no runtime answer, no stored state, no migration; phase-1 one-holder tests and bare-name URLs outside the diff.
  • Gates: check-runs at this head: 55 success, 11 skipped, 0 other (read this act). Local: lint build/typecheck/test 8836/8836, pnpm lint 0, dispatch-gates 59/59 --ran 0 NOT-MEASURED. Four source ablations + one CLI dist ablation red, restored to blob == HEAD.
  • Contract review: the diff touches none of the four faces (no packages/spec/src/**, no governed path, no (narrowing) arm, Clause-②: no), so CI plus this seat's read is the record; no second agent. The no cites the text it rests on (ADR-0150 D3 amendment note of 2026-11-11, ?callerPackage=P resolves from P), checked against the diff: the lint only stops refusing what that text and the runtime accept.
  • Changeset read sentence by sentence against the diff: the summary line, "read the judged stack's own datasets alone" (matches the removed recordsOf(stack.datasets)), Now, Still refused (both refusal arms and the warning arm match resolveDataset's new branch), The union run, Unchanged (no-packages[] and publish-door pins). All hold.
  • Accepted deviations: (1) the order pointed at packages/core/src/package-scoped-resolution.ts; the dashboard half actually resolves with the lint's indexArtifactDatasetResolution, which this PR imports (not copied) — the order's pointer was wrong, the dev's reading is right. (2) The one rule also resolves list-view and page charts: the same binding resolver, each reader sends its own package as callerPackage, pinned with CONTROLs. In scope of the card's rule.
  • Out-of-scope findings: validate-report-date-range.ts per-package pass indexes stack.datasets alone (no false refusal, union run judges it) → Acceptance notes. Filter keys on a dependency's objects skipped on the per-package pass, judged on the union run, same as the dashboard twin → Acceptance notes.
  • Landing: K = 6 (git rev-list --count --first-parent 47f6c939..origin/main), no packages/lint churn since base; no locale pack. Landed on the maintainer's standing word 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30 14:20Z, spec seat 2 thread): pr_ready + automerge_enable in this stroke.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants